/**
 * Copyright 2018-2019 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.modulelink.util.vfs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import com.jianggujin.modulelink.util.JLogFactory;
import com.jianggujin.modulelink.util.JLogFactory.JLog;

/**
 * 默认实现{@link JVFS}，适合大多数应用服务器
 * 
 * @author jianggujin
 *
 */
public class JDefaultVFS extends JVFS {
    private static final JLog logger = JLogFactory.getLog(JDefaultVFS.class);
    /** 表示JAR（zip）文件的魔法头 */
    private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };

    @Override
    public boolean isValid() {
        return true;
    }

    @Override
    public List<String> list(URL url, String path) throws IOException {
        InputStream is = null;
        try {
            List<String> resources = new ArrayList<String>();

            // 首先，尝试查找包含请求资源的JAR文件的URL，如果找到，然后将通过阅读JAR来列出子资源
            URL jarUrl = findJarForResource(url);
            if (jarUrl != null) {
                is = jarUrl.openStream();
                if (logger.isDebugEnabled()) {
                    logger.debug("Listing " + url);
                }
                resources = listResources(new JarInputStream(is), path);
            } else {
                List<String> children = new ArrayList<String>();
                try {
                    if (isJar(url)) {
                        // JBoss VFS有些版本可能提供一个Jar的流，即使URL引用的资源实际上并不是一个JAR
                        is = url.openStream();
                        JarInputStream jarInput = new JarInputStream(is);
                        if (logger.isDebugEnabled()) {
                            logger.debug("Listing " + url);
                        }
                        for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Jar entry: " + entry.getName());
                            }
                            children.add(entry.getName());
                        }
                        jarInput.close();
                    } else {
                        /**
                         * 一些servlet容器允许从目录资源中读取，比如文本文件，每行列出一个子资源。 然而，仅仅通过读取它们来区分目录和文件资源是没有办法的。
                         * 为了解决这一问题，在读取每一行时，尝试通过类加载器作为当前资源的子元素查找它。 如果任何一行失败，我们假设当前资源不是目录。
                         */
                        is = url.openStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
                        List<String> lines = new ArrayList<String>();
                        for (String line; (line = reader.readLine()) != null;) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Reader entry: " + line);
                            }
                            lines.add(line);
                            if (getResources(path + "/" + line).isEmpty()) {
                                lines.clear();
                                break;
                            }
                        }
                        reader.close();
                        if (!lines.isEmpty()) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Listing " + url);
                            }
                            children.addAll(lines);
                        }
                    }
                } catch (FileNotFoundException e) {
                    /*
                     * 文件的URL的openstream()调用可能会失败，这取决于servlet容器，因为目录无法打开阅读。如果出现这种情况， 则直接列出该目录。
                     */
                    if ("file".equals(url.getProtocol())) {
                        File file = new File(url.getFile());
                        if (logger.isDebugEnabled()) {
                            logger.debug("Listing directory " + file.getAbsolutePath());
                        }
                        if (file.isDirectory()) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Listing " + url);
                            }
                            children = Arrays.asList(file.list());
                        }
                    } else {
                        throw e;
                    }
                }

                // 在递归列出子资源时使用的URL前缀
                String prefix = url.toExternalForm();
                if (!prefix.endsWith("/")) {
                    prefix = prefix + "/";
                }

                // 遍历直系子女，添加文件到目录和检索
                for (String child : children) {
                    String resourcePath = path + "/" + child;
                    resources.add(resourcePath);
                    URL childUrl = new URL(prefix + child);
                    resources.addAll(list(childUrl, resourcePath));
                }
            }
            return resources;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Exception e) {
                }
            }
        }
    }

    /**
     * 列出在给定的{@link JarInputStream}中与指定的路径开始的条目列表
     * 
     * @param jar  jar输入流
     * @param path 开始的路径
     * @return 路径下条目列表
     * @throws IOException IO异常
     */
    protected List<String> listResources(JarInputStream jar, String path) throws IOException {
        // 在匹配名称时包括斜杠
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        if (!path.endsWith("/")) {
            path = path + "/";
        }

        // 遍历条目并收集以请求路径开头的条目
        List<String> resources = new ArrayList<String>();
        for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
            if (!entry.isDirectory()) {
                // 如果缺少"/"则添加
                String name = entry.getName();
                if (!name.startsWith("/")) {
                    name = "/" + name;
                }

                // 检查文件名称
                if (name.startsWith(path)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Found resource: " + name);
                    }
                    // 去除开头的"/"
                    resources.add(name.substring(1));
                }
            }
        }
        return resources;
    }

    /**
     * 试图解构给定URL以找到包含URL引用的资源的JAR文件。也就是说，假设URL引用一个JAR条目，这个方法将返回一个URL，
     * 该URL引用包含该条目的JAR文件。如果无法定位JAR，则此方法返回null。
     * 
     * @param url 需要解构的url
     * @return 可解构的URL
     * @throws MalformedURLException 错误的URL异常
     */
    protected URL findJarForResource(URL url) throws MalformedURLException {
        if (logger.isDebugEnabled()) {
            logger.debug("Find JAR URL: " + url);
        }

        // 如果URL的文件部分本身是一个URL，那么该URL可能指向JAR。
        try {
            for (;;) {
                url = new URL(url.getFile());
                if (logger.isDebugEnabled()) {
                    logger.debug("Inner URL: " + url);
                }
            }
        } catch (MalformedURLException e) {
            // 这将在某个时刻发生，并作为循环中的一个中断
        }

        // 寻找.jar扩展，然后删除其后内容
        StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
        int index = jarUrl.lastIndexOf(".jar");
        if (index >= 0) {
            jarUrl.setLength(index + 4);
            if (logger.isDebugEnabled()) {
                logger.debug("Extracted JAR URL: " + jarUrl);
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Not a JAR: " + jarUrl);
            }
            return null;
        }

        // 试着打开并测试
        try {
            URL testUrl = new URL(jarUrl.toString());
            if (isJar(testUrl)) {
                return testUrl;
            } else {
                // WebLogic修复: 检查URL文件是否存在于文件系统中
                if (logger.isDebugEnabled()) {
                    logger.debug("Not a JAR: " + jarUrl);
                }
                jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
                File file = new File(jarUrl.toString());

                // 文件名可能是URL编码的
                if (!file.exists()) {
                    try {
                        file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException("Unsupported encoding?  UTF-8?  That's unpossible.");
                    }
                }

                if (file.exists()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Trying real file: " + file.getAbsolutePath());
                    }
                    testUrl = file.toURI().toURL();
                    if (isJar(testUrl)) {
                        return testUrl;
                    }
                }
            }
        } catch (MalformedURLException e) {
            logger.warn("Invalid JAR URL: " + jarUrl);
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Not a JAR: " + jarUrl);
        }
        return null;
    }

    /**
     * 转换一个Java包名为可以被{@link ClassLoader#getResources(String)}使用的路径
     * 
     * @param packageName 包名称
     * @return 转换后的包路径
     */
    protected String getPackagePath(String packageName) {
        return packageName == null ? null : packageName.replace('.', '/');
    }

    /**
     * 如果位于给定URL的资源是JAR文件，则返回true
     * 
     * @param url 需要判断的JAR的URL
     * @return 给定URL的资源是JAR文件则返回true
     */
    protected boolean isJar(URL url) {
        return isJar(url, new byte[JAR_MAGIC.length]);
    }

    /**
     * 如果位于给定URL的资源是JAR文件，则返回true
     * 
     * @param url    需要判断的JAR的URL
     * @param buffer 缓冲区
     * @return 给定URL的资源是JAR文件则返回true
     */
    protected boolean isJar(URL url, byte[] buffer) {
        InputStream is = null;
        try {
            is = url.openStream();
            is.read(buffer, 0, JAR_MAGIC.length);
            if (Arrays.equals(buffer, JAR_MAGIC)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Found JAR: " + url);
                }
                return true;
            }
        } catch (Exception e) {
            // 读取失败则表示不是一个Jar文件 Failure to read the stream means this is not a JAR
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Exception e) {
                }
            }
        }
        return false;
    }
}
